/**
 * 
 */
package com.aurifa.struts2.plugin.image.validator;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.opensymphony.xwork2.validator.ValidationException;
import com.opensymphony.xwork2.validator.validators.FieldValidatorSupport;

/**
 * <p>
 * <!-- START SNIPPET: javadoc --> This is an image validator. You can use this
 * to validate images by passing urls or uploaded files. For now, you can
 * specify:
 * <ul>
 * <li>min/max dimensions</li>
 * <li>the mimetype (JPEG, GIF, BMP, PCX, PNG, IFF, RAS, PBM, PGM, PPM and PSD
 * formats are supported)</li>
 * <li>max filesize in bytes</li>
 * <li>if uploads and/or remote images (specified by url) are allowed</li>
 * </ul>
 * This class uses the excellent ImageInfo (written by Marco Schmidt), which
 * allows us to validate the image without fully loading it (when using urls).
 * <!-- END SNIPPET: javadoc -->
 * </p>
 * <p>
 * Possible parameters for this validator:
 * 
 * <!-- START SNIPPET: parameters -->
 * <ul>
 * <li>maxWidth - in pixels</li>
 * <li>minWidth - in pixels</li>
 * <li>maxHeight - in pixels</li>
 * <li>minHeight - in pixels</li>
 * <li>maxSize - in bytes</li>
 * <li>remote - boolean (default false)</li>
 * <li>upload - boolean (default false)</li>
 * <li>allowedMimeTypes - comma seperated list of allowed mime types</li>
 * </ul>
 * <!-- END SNIPPET: parameters -->
 * </p>
 * <p>
 * <!-- START SNIPPET: example --> Here's an example to validate an avatar for a
 * standard webbased forum:
 * 
 * <pre>
 *     	&lt;field name=&quot;avatar&quot;&gt;	
 *          	&lt;field-validator type=&quot;image&quot;&gt;
 *              	&lt;param name=&quot;maxWidth&quot;&gt;80&lt;/param&gt;
 *                 	&lt;param name=&quot;maxHeight&quot;&gt;80&lt;/param&gt;
 *                 	&lt;param name=&quot;minWidth&quot;&gt;40&lt;/param&gt;
 *                 	&lt;param name=&quot;minHeight&quot;&gt;40&lt;/param&gt;
 *                 	&lt;param name=&quot;maxSize&quot;&gt;40000&lt;/param&gt;
 *                 	&lt;param name=&quot;remote&quot;&gt;false&lt;/param&gt; &lt;!--only uploads allowed !--&gt;
 *                 	&lt;param name=&quot;allowedMimeTypes&quot;&gt;image/gif,image/jpg,image/png&lt;/param&gt;
 *                 	&lt;message key=&quot;uploaded.avatar&quot; /&gt;
 *              &lt;/field-validator&gt;
 *     	&lt;/field&gt;
 * </pre>
 * 
 * </p>
 * <!-- END SNIPPET: example --> <!-- START SNIPPET: error -->
 * <p>
 * The keys you should be using for your localised error reporting are always
 * made of the original key you provided in the validator + the possible error.
 * So, if you have a key named 'uploaded.avatar' (=key_name), and the image
 * would be too wide, you would get a message key back named
 * 'uploaded.avatar.invalid.width.max'
 * 
 * The other keys:
 * <ul>
 * <li>key_name + 'invalid': the provided image was not valid</li>
 * <li>key_name + 'invalid.location': the remote image location should start with http://</li>
 * <li>key_name + 'invalid.notallowed': the requested action (remote or fileupload) is not allowed</li>
 * <li>key_name + 'invalid.notfound': the image could not be located</li>
 * <li>key_name + 'invalid.mime': the mimetype of the image was not listed in
 * the allowedMimeTypes parameter</li>
 * <li>key_name + 'invalid.width.min': the image's width didn't meet the
 * required minimal width in pixels</li>
 * <li>key_name + 'invalid.width.max': the image's width didn't meet the
 * required maximal width in pixels</li>
 * <li>key_name + 'invalid.height.min': the image's height didn't meet the
 * required minimal height in pixels</li>
 * <li>key_name + 'invalid.height.max': the image's height didn't meet the
 * required maximal height in pixels</li>
 * <li>key_name + 'invalid.size': the image file size was too big</li>
 * 
 * </ul>
 * <!-- END SNIPPET: error -->
 * </p>
 * 
 * @author <a href="mailto:philip.luppens@gmail.com">Philip Luppens</a>
 * @version 1.0
 */
public class CombinedImageValidator extends FieldValidatorSupport {

	private final static Log logger = LogFactory
			.getLog(CombinedImageValidator.class);

	private Integer maxWidth, maxHeight;

	private Integer minWidth, minHeight;

	private Long maxSize;

	private int imageWidth, imageHeight, imageSize;

	private String imageMimeType;
	
	private boolean remote, upload = false;

	private List allowed;

	private ImageInfo ii;

	// tokens of mimetypes, seperated by commas
	private String allowedMimeTypes;

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.opensymphony.xwork.validator.Validator#validate(java.lang.Object)
	 */
	public void validate(Object object) throws ValidationException {
		allowed = tokenizeMimeTypes();
		String fieldName = getFieldName();
		
		Object fieldValue = getFieldValue(fieldName, object);
		
		if (fieldValue != null) {

			Object[] images;
			if (fieldValue.getClass().isArray()) {
				images = (Object[]) fieldValue;
			} else {
				images = new Object[] { fieldValue };
			}
			for (int i = 0; i < images.length; i++) {
				if (logger.isDebugEnabled()) {
					logger.debug("Image: " + images[i]);
				}
			}
			for (int i = 0; i < images.length; i++) {
				ii = new ImageInfo();
				if (logger.isDebugEnabled()) {
					logger.debug("Validating Image: " + images[i]);
				}
				if (!isValid(images[i])) {
					addFieldError(fieldName, object);
					return;
				}
			}

		} else {
			logger.warn("Field value not found");
			addFieldError(fieldName, object);
			return;
		}
	}

	/*
	 * Simply tokenize the allowedMimeTypes String from our validator
	 * parameters.
	 * 
	 */
	private final List tokenizeMimeTypes() {
		// make sure we don't get a npe
		if (allowedMimeTypes == null) {
			return new ArrayList();
		}
		StringTokenizer st = new StringTokenizer(allowedMimeTypes, ",");
		List<String> types = new ArrayList<String>();
		while (st.hasMoreTokens()) {
			types.add((st.nextToken()).trim());
		}
		return types;
	}

	/**
	 * This method will try to validate an image Object. Be aware that this
	 * could mean it is a String with the location or an uploaded File.
	 * 
	 * @param image
	 * @return
	 * @throws ValidationException
	 */
	public boolean isValid(Object image) throws ValidationException {

		if (image instanceof String) {
			if (!remote){
				//we're not allowed to pass remote images
				setMessageKey(getMessageKey() + ".invalid.notallowed");
				// no need to continue
				return false;
			}
			// ah, this is a remote file location
			if (((String) image).toLowerCase().startsWith("http://")) {

				URL url = null;
				try {
					url = new URL((String) image);
					// set up our image info stream
					ii.setInput(url.openStream());
				} catch (Exception e) {
					logger.error(e);
					setMessageKey(getMessageKey() + ".invalid.notfound");
					// no need to continue
					return false;
				}
				// image's filesize is checked by requesting the content
				// length header
				try {
					imageSize = url.openConnection().getContentLength();
				} catch (Exception e) {
					logger.error(e);
					// could not check the content length ?
					setMessageKey(getMessageKey() + ".invalid.size");
					// no need to continue
					return false;
				}
			} else {
				//non-remote urls are not accepted
				setMessageKey(getMessageKey() + ".invalid.location");
				return false;
			}

		} else if (image instanceof File) {
			if (!upload){
				//we're not allowed to upload images
				setMessageKey(getMessageKey() + ".invalid.notallowed");
				// no need to continue
				return false;
			}
			
			// file upload
			File i = (File) image;
			try {
				ii.setInput(new FileInputStream(i));
			} catch (FileNotFoundException e) {
				// could not find the file ? Highly unlikely, but it *could*
				// happen
				// This is a critical exception, so we better throw a
				// ValidationException
				// no need to continue
				logger.error(e);
				throw new ValidationException("Could not open inputstream for "
						+ i);
			}
			imageSize = (int) i.length();
		} else {
			// it's .. uhm, something else. A football ? A hot blonde's address
			// ? Fourty Two ? We'll never know ..
			throw new ValidationException(
					"Invalid object. Expect either a String or File object. Object is "
							+ image.getClass().getName());
		}
		if (!ii.check()) {
			setMessageKey(getMessageKey() + ".invalid");
			return false;
		}
		// check if the mimetype is allowed
		imageMimeType = ii.getMimeType();
		if (!allowed.contains(imageMimeType)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid mimetype: " + ii.getMimeType());
			}
			setMessageKey(getMessageKey() + ".invalid.mime");
			return false;
		}
		imageWidth = ii.getWidth();
		imageHeight = ii.getHeight();

		// not entirely sure about the best error reporting way ..

		if (maxWidth != null && ii.getWidth() > maxWidth.intValue()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid width: " + ii.getWidth());
			}
			setMessageKey(getMessageKey() + ".invalid.width.max");
			return false;
		}
		if (minWidth != null && ii.getWidth() < minWidth.intValue()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid width: " + ii.getWidth());
			}
			setMessageKey(getMessageKey() + ".invalid.width.min");
			return false;
		}
		if (maxHeight != null && ii.getHeight() > maxHeight.intValue()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid height: " + ii.getHeight());
			}
			setMessageKey(getMessageKey() + ".invalid.height.max");
			return false;
		}
		if (minHeight != null && ii.getHeight() < minHeight.intValue()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid height: " + ii.getHeight());
			}
			setMessageKey(getMessageKey() + ".invalid.height.min");
			return false;
		}
		if (maxSize != null && imageSize > maxSize.longValue()) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invalid size: " + imageSize);
			}
			setMessageKey(getMessageKey() + ".invalid.size");
			return false;
		}
		return true;
	}

	public Integer getMaxHeight() {
		return maxHeight;
	}

	public void setMaxHeight(Integer maxHeight) {
		this.maxHeight = maxHeight;
	}

	public Long getMaxSize() {
		return maxSize;
	}

	public void setMaxSize(Long maxSize) {
		this.maxSize = maxSize;
	}

	public Integer getMaxWidth() {
		return maxWidth;
	}

	public void setMaxWidth(Integer maxWidth) {
		this.maxWidth = maxWidth;
	}

	public String getAllowedMimeTypes() {
		return allowedMimeTypes;
	}

	public void setAllowedMimeTypes(String allowedMimeTypes) {
		this.allowedMimeTypes = allowedMimeTypes;
	}

	// image properties after processing
	public int getImageHeight() {
		return imageHeight;
	}

	public String getImageMimeType() {
		return imageMimeType;
	}

	public int getImageSize() {
		return imageSize;
	}

	public int getImageWidth() {
		return imageWidth;
	}

	public Integer getMinHeight() {
		return minHeight;
	}

	public void setMinHeight(Integer minHeight) {
		this.minHeight = minHeight;
	}

	public Integer getMinWidth() {
		return minWidth;
	}

	public void setMinWidth(Integer minWidth) {
		this.minWidth = minWidth;
	}

	/**
	 * @return Returns the remote.
	 */
	public boolean isRemote() {
		return remote;
	}

	/**
	 * @param remote The remote to set.
	 */
	public void setRemote(boolean remote) {
		this.remote = remote;
	}

	/**
	 * @return Returns the upload.
	 */
	public boolean isUpload() {
		return upload;
	}

	/**
	 * @param upload The upload to set.
	 */
	public void setUpload(boolean upload) {
		this.upload = upload;
	}

}
